
package com.trydofor.id.a9cq;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.concurrent.ThreadSafe;

import com.trydofor.id.core.A9CQ;
import com.trydofor.id.core.ApiExceptions;

/**
 * an atomic extendible entry a9cq.
 * protected by ReadWriteLock.
 * 
 * <pre>
 * <b>write</b> action are:
 *  - next()
 *  - next(long, TimeUnit)
 *  - reset()
 *  - append(Object, Object...)
 *  - append(Collection)
 * <b>read</b> action are:
 *  - current()
 *  - remains()
 *  - toString()
 *  
 * when calling next(...), the CQ will remove the used up entry 
 * if the count of entry is greater than the capacity.
 * so, the capacity(N) is
 * - positive - kept the last N entries in the end. 
 * - zero     - kept nothing
 * - negative - kept all entries.
 * </pre>
 * 
 * @param <E> the Element of sequence
 * @author Shi Rongjiu (www.trydofor.com)
 * @since 0.9
 */
@ThreadSafe
public final class ExtedEntryA9cq<E> implements A9CQ<E> {
    
    // lock
    private final ReadWriteLock rwLock       = new ReentrantReadWriteLock();
    private final Lock          rLock        = rwLock.readLock();
    private final Lock          wLock        = rwLock.writeLock();
    
    //
    private final boolean       isCyclic;
    private final int           capacity;
    
    // sync
    private final LinkedList<E> readWriteBox = new LinkedList<E>();
    private volatile int        currentIndex = 0;
    private volatile E          currentValue = null;
    
    /**
     * an atomic extendible entry a9cq. by capacity(0).
     * 
     * @param isCyclic cyclic generation or not.
     * @see #ExtedEntryA9cq(boolean, int)
     */
    public ExtedEntryA9cq(boolean isCyclic) {
        this(isCyclic, 0);
    }
    
    /**
     * an atomic extendible entry a9cq.
     * 
     * @param isCyclic cyclic generation or not.
     * @param capacity how many entries can be kept in the end.
     */
    public ExtedEntryA9cq(boolean isCyclic, int capacity) {
        this.isCyclic = isCyclic;
        this.capacity = capacity;
    }
    
    /**
     * whether cyclically generate sequence.
     * 
     * @return true if cyclic.
     */
    public boolean isCyclic() {
        return isCyclic;
    }
    
    /**
     * {@inheritDoc}
     */
    public boolean isAtomic() {
        return true;
    }
    
    /**
     * how many entries can be kept in the end.
     * 
     * @return the capacity
     */
    public int getCapacity() {
        return capacity;
    }
    
    /**
     * protected by read lock.
     * 
     * {@inheritDoc}
     */
    public long remains() {
        rLock.lock();
        try {
            int count = readWriteBox.size();
            return isCyclic ? count : count - currentIndex;
        }
        finally {
            rLock.unlock();
        }
    }
    
    /**
     * protected by read lock.
     * 
     * {@inheritDoc}
     */
    public E current() {
        rLock.lock();
        try {
            return currentValue;
        }
        finally {
            rLock.unlock();
        }
    }
    
    /**
     * protected by write lock.
     * 
     * {@inheritDoc}
     */
    public E next() {
        E r = null;
        try {
            r = next(0, null);
        }
        catch (InterruptedException e) {
            ApiExceptions.throwFakeException(e);
        }
        catch (TimeoutException e) {
            ApiExceptions.throwFakeException(e);
        }
        return r;
    }
    
    /**
     * protected by write lock.
     * 
     * {@inheritDoc}
     */
    public E next(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
        final long startedNano = System.nanoTime();
        final Thread curThread = Thread.currentThread();
        final boolean needTime = (timeout > 0 && unit != null);
        
        final E result;
        wLock.lock();
        try {
            // pre-check
            if (needTime) {
                ApiExceptions.checkThreadInterrupted(curThread);
                ApiExceptions.checkTimeoutNano(startedNano, timeout, unit);
            }
            
            int varIndex = currentIndex;
            if (readWriteBox.size() <= 0) { // empty
                varIndex = 0;
                result = null;
            }
            else {
                if (capacity < 0 || capacity >= readWriteBox.size()) {
                    if (varIndex < readWriteBox.size()) {
                        result = readWriteBox.get(varIndex);
                        varIndex++;
                    }
                    else {
                        result = isCyclic ? readWriteBox.get(0) : null;
                        varIndex = 1;
                    }
                }
                else {
                    result = readWriteBox.removeFirst();
                    varIndex = 0;
                }
            }
            
            // change shared value
            currentValue = result;
            currentIndex = varIndex;
        }
        finally {
            wLock.unlock();
        }
        
        // post-check
        if (needTime) {
            ApiExceptions.checkThreadInterrupted(curThread);
            ApiExceptions.checkTimeoutNano(startedNano, timeout, unit);
        }
        return result;
    }
    
    /**
     * protected by write lock.
     * 
     * {@inheritDoc}
     */
    public void reset() {
        wLock.lock();
        try {
            currentIndex = 0;
            currentValue = null;
        }
        finally {
            wLock.unlock();
        }
    }
    
    /**
     * protected by write lock.
     * 
     * append some values to the end.
     * 
     * @param e the first appending value.
     * @param ts the others appending values.
     * @return the count of appended values.
     * @see #append(Collection)
     */
    public int append(E e, E... ts) {
        List<E> ls = new ArrayList<E>(ts.length + 1);
        ls.add(e);
        for (int i = 0; i < ts.length; i++) {
            ls.add(ts[i]);
            
        }
        return append(ls);
    }
    
    /**
     * protected by write lock.
     * 
     * append some values to the end.
     * 
     * @param ts the appending values.
     * @return the count of appended values.
     */
    public int append(Collection<E> ts) {
        wLock.lock();
        try {
            // try to add new
            int count = 0;
            for (E t : ts) {
                if (!readWriteBox.add(t)) break;
                count++;
            }
            
            // remove old
            if (capacity > 0 && capacity < readWriteBox.size()) {
                while (currentIndex > 0) {
                    readWriteBox.removeFirst();
                    currentIndex--;
                }
            }
            
            return count;
        }
        finally {
            wLock.unlock();
        }
    }
    
    /**
     * protected by read lock.
     * 
     * {@inheritDoc}
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("isCyclic=");
        sb.append(isCyclic);
        sb.append(",capacity=");
        sb.append(capacity);
        rLock.lock();
        try {
            int count = readWriteBox.size();
            sb.append(",remains=");
            sb.append((isCyclic ? count : count - currentIndex));
            sb.append(",current=");
            sb.append(currentValue);
        }
        finally {
            rLock.unlock();
        }
        return sb.toString();
    }
}
